package com.one.blocks.rbac.log;

import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.EventTranslatorOneArg;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.one.blocks.common.util.JsonUtil;
import com.one.blocks.common.util.LocalDateTimeUtil;
import com.one.blocks.rbac.domain.SysLog;
import com.one.blocks.rbac.util.SubjectHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.InitializingBean;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author linfuxin Created on 2022-02-14 11:20:28
 */
@Slf4j
@Aspect
public class LogAspect implements InitializingBean {

    private final int bufferSize;

    private final DataSource dataSource;

    private EventProducerWithTranslator producer;

    public LogAspect(int bufferSize, DataSource dataSource) {
        // TODO 参考hashmap设置最小值限定
        this.bufferSize = bufferSize;
        this.dataSource = dataSource;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Disruptor<SysLogEvent> disruptor = new Disruptor<>(SysLogEvent::new, bufferSize, new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("webLogAop-" + threadNumber.getAndIncrement());
                thread.setDaemon(true);
                return thread;
            }
        });
        // 只能单消费者模式，多消费者SysLogHandler有线程安全问题
        disruptor.handleEventsWith(new SysLogHandler(bufferSize, dataSource));
        disruptor.start();

        // 获取环形队列，用于生产事件
        RingBuffer<SysLogEvent> ringBuffer = disruptor.getRingBuffer();
        producer = new EventProducerWithTranslator(ringBuffer);
    }

    @Pointcut(value = "@annotation(com.one.blocks.rbac.log.Log)")
    public void logPointCut() {
    }

    @Around("logPointCut() && @annotation(logAnnotation)")
    public Object log(ProceedingJoinPoint joinPoint, Log logAnnotation) throws Throwable {
        // 获取请求参数
        Object[] args = joinPoint.getArgs();
        Log classLogAnnotation = joinPoint.getTarget().getClass().getAnnotation(Log.class);
        try {
            Object result = joinPoint.proceed();
            doLog(classLogAnnotation, logAnnotation, args, result);
            return result;
        } catch (Throwable e) {
            doLog(classLogAnnotation, logAnnotation, args, e);
            throw e;
        }
    }

    private void doLog(Log classLogAnnotation, Log logAnnotation, Object[] args, Object result) {
        SysLog sysLog = buildData(classLogAnnotation, logAnnotation, args, result);
        if (logAnnotation.save()) {
            // 发布事件
            producer.onData(sysLog);
        } else {
            log.info("请求参数={},响应结果={}", sysLog.getParam(), sysLog.getResult());
        }
    }

    private static final String[] DEFAULT_PARAM = {"void"};

    private SysLog buildData(Log classLogAnnotation, Log logAnnotation, Object[] args, Object result) {
        // module设置class值
        SysLog data = new SysLog();
        data.setBizModule(classLogAnnotation != null && StringUtils.isBlank(logAnnotation.module()) ? classLogAnnotation.module() : logAnnotation.module());
        data.setOperateType(logAnnotation.operateType().getCode());

        Object[] param = ArrayUtils.isEmpty(logAnnotation.argIndex()) ? DEFAULT_PARAM : cloneParam(args, logAnnotation.argIndex());
        data.setParam(param.length == 1 ? JsonUtil.toString(param[0]) : JsonUtil.toString(param));
        data.setResult(result instanceof Throwable ? ((Throwable) result).getMessage() : JsonUtil.toString(result));
        data.setCreateId(SubjectHelper.getSubjectId());
        data.setCreateTime(SubjectHelper.getAccessTime());
        return data;
    }

    private Object[] cloneParam(Object[] args, int[] idxArray) {
        if (idxArray.length == 1) {
            return new Object[]{args[idxArray[0]]};
        }
        Object[] params = new Object[idxArray.length];
        for (int i = 0, length = idxArray.length; i < length; i++) {
            params[i] = args[idxArray[i]];
        }
        return params;
    }

    static class EventProducerWithTranslator {

        private static final EventTranslatorOneArg<SysLogEvent, SysLog> TRANSLATOR =
                (event, sequence, data) -> event.setData(data);

        private final RingBuffer<SysLogEvent> ringBuffer;

        public EventProducerWithTranslator(RingBuffer<SysLogEvent> ringBuffer) {
            this.ringBuffer = ringBuffer;
        }

        public void onData(SysLog data) {
            ringBuffer.publishEvent(TRANSLATOR, data);
        }
    }

    static class SysLogHandler implements EventHandler<SysLogEvent> {

        private final long bufferSize;
        private final DataSource dataSource;
        private final List<SysLog> cache;

        private static final String INSERT_SQL = "insert into sys_log(biz_module,operate_type,param,result,create_id,create_time) values(?,?,?,?,?,?)";

        public SysLogHandler(int bufferSize, DataSource dataSource) {
            this.bufferSize = bufferSize / 2;
            this.dataSource = dataSource;
            this.cache = new ArrayList<>(bufferSize);
        }

        @Override
        public void onEvent(SysLogEvent event, long sequence, boolean endOfBatch) throws Exception {
            try {
                cache.add(event.getData());
                if (endOfBatch || ((sequence + 1) & bufferSize) == 0) {
                    // 触发保存条件
                    batchSave();
                }
            } catch (Exception e) {
                log.error("系统日志保存失败!data={},e={}", event.getData(), e.getMessage());
            }
        }

        private void batchSave() throws SQLException {
            // mybatis插入后有个查询主键填充对象操作，在批量模式下会失效，这里考虑兼容与性能改为直接jdbc操作
            try (Connection connection = dataSource.getConnection()) {
                connection.setAutoCommit(false);
                try (PreparedStatement ps = connection.prepareStatement(INSERT_SQL)) {
                    for (SysLog item : cache) {
                        ps.setString(1, item.getBizModule());
                        ps.setInt(2, item.getOperateType());
                        ps.setString(3, item.getParam());
                        ps.setString(4, item.getResult());
                        ps.setInt(5, item.getCreateId());
                        ps.setDate(6, LocalDateTimeUtil.datetimeToSqlDate(item.getCreateTime()));
                        ps.addBatch();
                    }
                    ps.executeBatch();
                    connection.commit();
                } catch (SQLException e) {
                    connection.rollback();
                }
            }
        }
    }

}